前面幾集我們搞定了網站後台的 Session 登入,但如果你的服務需要讓手機 App 或其他「機器」來存取,Session 就不好用了。這時候,我們需要更酷、更現代的魔法小卡:JWT (JSON Web Token)!
📅 開發設定筆記
在開始之前,統一一下我們的基礎設定:
時區:Asia/Taipei(+08:00)
日期格式:2006-01-02
✏️ 這次改了什麼?(重點整理)
想像你正在組裝一台樂高機器人,這次我們加了幾個超重要的零件:
新增 🔐 登入關卡:api_auth.go (處理登入,發給你魔法小卡的地方)
新增 👑 管理者專區:api_admin.go (只有有卡的人才能進去)
調整 🛣️ 交通總部:main.go (設定「認卡」的檢查站)
新增 🗝️ 密碼鎖:.env.example (給 JWT 魔法小卡加一個超級難的密碼鎖 JWT_SECRET)
這個檔案負責接收使用者的 E-mail 和密碼,進行驗證後,發放一張有期限的 JWT 魔法小卡 (access_token)。
我們設定卡片只有 15 分鐘 的壽命 (15 * time.Minute)。
package handlers
import (
"net/http"
"os"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/labstack/echo/v4"
"golang.org/x/crypto/bcrypt"
)
type APIAuthHandler struct {
Users UsersRepo // 從第 7 篇沿用:FindByEmail(ctx, email)
}
func NewAPIAuthHandler(users UsersRepo) *APIAuthHandler {
return &APIAuthHandler{Users: users}
}
type tokenReq struct {
Email string `json:"email" form:"email"`
Password string `json:"password" form:"password"`
}
type tokenResp struct {
AccessToken string `json:"access_token"`
ExpiresIn int64 `json:"expires_in"`
}
func (h *APIAuthHandler) IssueToken(c echo.Context) error {
var req tokenReq
if err := c.Bind(&req); err != nil {
return c.JSON(http.StatusBadRequest, echo.Map{"error": "bad request"})
}
// 驗證 E-mail 和密碼
u, err := h.Users.FindByEmail(c, req.Email)
if err != nil || bcrypt.CompareHashAndPassword([]byte(u.PasswordHash), []byte(req.Password)) != nil {
return c.JSON(http.StatusUnauthorized, echo.Map{"error": "invalid credentials"})
}
// 設定 15 分鐘過期
exp := time.Now().Add(15 * time.Minute)
claims := jwt.MapClaims{
"uid": u.ID,
"role": u.Role,
"exp": exp.Unix(),
"iat": time.Now().Unix(),
"iss": "go-echo-blog", // 發行者
}
t := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
// 用 JWT_SECRET 簽名加密
secret := []byte(os.Getenv("JWT_SECRET"))
signed, err := t.SignedString(secret)
if err != nil {
return c.JSON(http.StatusInternalServerError, echo.Map{"error": "sign token failed"})
}
return c.JSON(http.StatusOK, tokenResp{
AccessToken: signed,
ExpiresIn: int64(time.Until(exp).Seconds()),
})
}
這個檔案是受保護的 API 邏輯。它會從 echo.Context 中取出經由 Middleware 驗證後解析出來的 JWT 資訊,並傳回使用者的 uid 和 role。
package handlers
import (
"net/http"
"github.com/golang-jwt/jwt/v5"
"github.com/labstack/echo/v4"
)
type APIAdminHandler struct{}
func NewAPIAdminHandler() *APIAdminHandler { return &APIAdminHandler{} }
// GET /api/admin/secret
func (h *APIAdminHandler) Secret(c echo.Context) error {
// 從 Context 取得 JWT 解析後的資訊
user := c.Get("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
return c.JSON(http.StatusOK, echo.Map{
"message": "歡迎來到管理者 API",
"uid": claims["uid"],
"role": claims["role"],
})
}
在這裡,我們設定了發卡路徑,並使用 middleware.JWTWithConfig 建立了一個 JWT 檢查站,將所有 /api/admin 開頭的路由都包在這個檢查站裡。
package main
import (
"net/http"
"os"
"github.com/gorilla/sessions"
"github.com/labstack/echo-contrib/session"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"your/module/internal/http/handlers"
)
func main() {
e := echo.New()
e.Use(middleware.Recover())
e.Use(middleware.Logger())
e.Use(middleware.CORS()) // 開發先放寬,正式白名單
// 後台 Session(沿用第 7 篇的設定)
e.Use(session.Middleware(sessions.NewCookieStore([]byte(os.Getenv("SESSION_KEY")))))
// ===== JWT 區 - 驗證與路由設定 =====
usersRepo := buildUsersRepo() // 你的實作 (請確保你有實作這個函式)
apiAuth := handlers.NewAPIAuthHandler(usersRepo)
// 登入路徑:POST /api/auth/token
e.POST("/api/auth/token", apiAuth.IssueToken)
// 設置 JWT 檢查站設定
jwtCfg := middleware.JWTConfig{
SigningKey: []byte(os.Getenv("JWT_SECRET")), // 簽名金鑰
TokenLookup: "header:Authorization", // Token 會從 HTTP Header 的 Authorization 欄位取
AuthScheme: "Bearer", // 格式為 Authorization: Bearer <token>
}
// 設定 /api/admin 路由群組,並強制通過 JWT 檢查站
apiAdmin := e.Group("/api/admin", middleware.JWTWithConfig(jwtCfg))
admin := handlers.NewAPIAdminHandler()
apiAdmin.GET("/secret", admin.Secret)
// 健檢
e.GET("/health", func(c echo.Context) error { return c.String(http.StatusOK, "ok") })
e.GET("/_ping", func(c echo.Context) error { return c.String(http.StatusOK, "pong") })
e.Logger.Fatal(e.Start(":1323"))
}
記得在你的 .env 檔案中加入這個秘密金鑰!請務必修改 please_change_me_now 成一串夠長、夠亂的密碼!
JWT_SECRET=please_change_me_now
✅ 驗收清單(DoD)
完成後,請用工具(如 Postman 或 cURL)測試,確保流程正確:
成功發卡:POST /api/auth/token 能拿到 access_token 與 expires_in。
拒絕訪問:不帶或帶錯誤的 Token 打 /api/admin/* → 收到 401 Unauthorized。
成功訪問:帶正確 Token 打 /api/admin/secret → 看到自己的 uid/role。
💡 小叮嚀(安全與維運)
雖然 JWT 方便,但有些細節要注意,才不會被駭客有機可乘:
Token 設短效:我們設了 15 分鐘,如果 Token 被偷了,損失也會被限制在這個時間內。
前端請藏好:前端(網頁/App)拿到 Token 後,請放在記憶體或安全容器中,避免存放在 LocalStorage,減少被 XSS 攻擊偷走的風險。
強制登出:如果需要「一鍵讓全部裝置登出」的功能,請研究 token_version 版本號法,在 Token 裡加入一個版本號,Middleware 驗證版本不一致就拒絕,這樣就能隨時讓舊卡失效。
結語:現在你的服務已經能同時以兩種模式運作:
後台用 Session (服務「人」)
API 用 JWT (服務「機器」)
兩條線跑起來,你的服務擴展性就大大提升了!